package dk.iha;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import dk.iha.BlackBoxBTSensor.BlackBoxData;

import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;


/* 
 * SensorMain maintains links to all sensor handlers (derived from ISensor)
*/
public class SensorMain {

	private SensorHandler accelerometerSensorHandler = null;
	private SensorHandler batteryIndicatorSensorHandler = null; 
	private SensorHandler gpsSensorHandler = null;
	private SensorHandler BlackBoxBTSensorHandler = null;
	private List<SensorHandler> sensorHandlers = null;
	private SensorLoggerDatabase db = null;
	private String uniqueId = null;
	private Handler completionHandler = null;
	private Integer GPSIntervalCntr;
	private String baseFolder = null;
	private String time;
	private BluetoothDevice btDevice;
	
	public void Initialize(Context ctx, 
						   String uniqueId,
						   SensorLoggerDatabase sensorLoggerDatabase, 
						   Handler CompleteHandler, 
						   String baseFolder, 
						   BluetoothDevice dev) {
		this.completionHandler 	= CompleteHandler;
		this.uniqueId 			= uniqueId;
		this.GPSIntervalCntr	= 0;
		this.db 				= sensorLoggerDatabase;
		this.baseFolder 		= baseFolder;
		this.btDevice 			= dev;
		
		//create SensorHandler structure
		sensorHandlers = new ArrayList<SensorHandler>();

		//
		//initialise sensors
		//
		//initialise Bluetooth sensor
		if(SharedPreferences.ENABLE_BTSENSOR && btDevice != null){
			BlackBoxBTSensorHandler = new SensorHandler(new BlackBoxBTSensor(btDevice), 
														onSensorCompleteHandler, 
														SharedPreferences.SAMPLE_PERIOD_BTDEVICE, 
														1);
			sensorHandlers.add(BlackBoxBTSensorHandler);
		}		
		
		//initialise accelerometer sensor
		accelerometerSensorHandler = new SensorHandler(new AccelerometerSensor(ctx),
													   onSensorCompleteHandler,
													   SharedPreferences.SAMPLE_PERIOD_ACCELEROMETER,
													   1);
		sensorHandlers.add(accelerometerSensorHandler);
		
		
		//perform extra initialisation of the accelerometer data file
		initializeAccelerometerDataFile();

		//Initialise battery sensor
		batteryIndicatorSensorHandler = new SensorHandler(new BatteryIndicatorSensor(ctx),
														   onSensorCompleteHandler,
														   0, //time out value
														   1);
		sensorHandlers.add(batteryIndicatorSensorHandler);
		
		
		//initialise GPSSensor
		if(SharedPreferences.ENABLE_GPS){
			gpsSensorHandler = new SensorHandler(new GPSSensor(ctx), 
												 onSensorCompleteHandler, 
												 SharedPreferences.SAMPLE_PERIOD_GPS, 
												 SharedPreferences.GPS_INTERVAL);
			sensorHandlers.add(gpsSensorHandler);
		}
	}

	
	
	/**
	 * Called from the SensorHandler to signal that the
	 * SensorHandler/Sensor has run for the specified amount of time 
	 */
	Handler onSensorCompleteHandler = new Handler() {
		public void handleMessage(Message msg) {
			//check if any other sensor is running if any is - then do nothing 
			for(SensorHandler sh : sensorHandlers){
				if(sh.isRunning())
					return;
			}
			//no other sensor is running!
			//if any sensors hasData then call finaliseSensorDataCollection  
			for(SensorHandler sh : sensorHandlers){
				if(sh.hasData())
					finaliseSensorDataCollection();
				break;
			}
		};
	};

	
	/**
	 * Starts datacollection on all sensors 
	 */
	public void startDataCollection(){
		//SharedData.getInstance().addDebugDataLog_t("SensorMain.startDataCollection");
		Date now = new Date();
		SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy", Locale.US);
		time = dateFormat.format(now);

		//increment GPS interval counter
		GPSIntervalCntr++;
		
		//start all sensors
		for(SensorHandler sh : sensorHandlers){
			sh.start();
		}
	}


	/**
	 * Called from outside to stop data collection
	 */
	public void externalStop(){
		//Stop logging
		for(SensorHandler sh : sensorHandlers){
			sh.stop();
		}
		//only finalise when any sensor has data (if no one is running then it means that all has been finalised)  
		for(SensorHandler sh : sensorHandlers){
			if(sh.hasData()){
				finaliseSensorDataCollection();
				break;
			}
		}
	}

	/**
	 * Writes data to the db
	 * All sensor implementers must implement a special handling for their specific sensor
	 * 
	 */
	@SuppressWarnings("unused")
	private void finaliseSensorDataCollection() {
		//
		//Handle GPS data STARTS
		//
		String latitude = SharedPreferences.NOT_AVAILABLE;
		String longitude = SharedPreferences.NOT_AVAILABLE;
		if(SharedPreferences.ENABLE_GPS && gpsSensorHandler.hasData()){
			GPSSensor gpsSensor = (GPSSensor) gpsSensorHandler.getSensor();
			Double lat = round(gpsSensor.getPosition().getLatitude(), 5, BigDecimal.ROUND_HALF_UP);
			Double lon = round(gpsSensor.getPosition().getLongitude(), 5, BigDecimal.ROUND_HALF_UP);
			latitude = lat.toString();
			longitude = lon.toString();
			//SharedData.getInstance().addDebugDataLog_t("position data - lat: " + latitude + " longi: " + longitude);
		}
		//
		//Handle GPS data ENDS
		//

		//
		//Handle battery data STARTS
		//
		String battery_data = SharedPreferences.NOT_AVAILABLE;
		if(batteryIndicatorSensorHandler.hasData()){
			BatteryIndicatorSensor batteryIndicatorSensor = (BatteryIndicatorSensor)batteryIndicatorSensorHandler.getSensor();  
			float result = round(batteryIndicatorSensor.getBatteryLevel(), 0, BigDecimal.ROUND_HALF_UP);
			Integer res = (int)result; 
			battery_data = res.toString();
		}

		//
		//Handle battery data ENDS
		//

		//
		//Handle accelerometer data STARTS
		//
		String ratingStr = SharedPreferences.NOT_AVAILABLE;
		if(accelerometerSensorHandler.hasData()){
			AccelerometerSensor accelerometerSensor = (AccelerometerSensor) accelerometerSensorHandler.getSensor();
			List<Float> x_data = accelerometerSensor.getData_x();
			List<Float> y_data = accelerometerSensor.getData_y();
			List<Float> z_data = accelerometerSensor.getData_z();
			//if dataset is large enough?
			if(isDatasetLargeEnough(x_data)){
				enumMovementRating rating = weighData(x_data, y_data, z_data);
				ratingStr = rating == enumMovementRating.LOW ? "Low" : rating == enumMovementRating.MEDIUM ? "Med" : "High";  
				//export raw data to file
				exportAccelerometerData(time, ratingStr, x_data, y_data, z_data);
				
				//SharedData.getInstance().addDebugDataLog_t("ADD DATA TO DB - "+ratingStr);
			}
			else{
				SharedData.getInstance().addDebugDataLog_t("stopSampling : NOT ENOUGH DATA!");
			}
		}
	    //
		//Handle accelerometer data ENDS
		//
		
		
		//
		//Handle BlackBox data STARTS
		//
		String blackBoxStr = SharedPreferences.NOT_AVAILABLE;
		if(SharedPreferences.ENABLE_BTSENSOR && btDevice != null && BlackBoxBTSensorHandler.hasData()){
			BlackBoxBTSensor bbSensor = (BlackBoxBTSensor) BlackBoxBTSensorHandler.getSensor();
			List<BlackBoxData> data = bbSensor.getData();
			if(!data.isEmpty()){
				//calc temperature
				int temp = data.get(0).temperature;
				double flTemp = temp*.2222-61.111;
				blackBoxStr = String.format("%.1f", flTemp);
			}
			Log.w("WW","Temperature:"+blackBoxStr);
		}
	    //
		//Handle BlackBox data ENDS
		//
		
		
		
		//write values to db
		db.addNewLogEntry(new LogItem(uniqueId, time, ratingStr, latitude, longitude, battery_data, blackBoxStr));
		
		
		//reset hasData flag on all SensorHandlers
		for(SensorHandler sh : sensorHandlers){
			sh.reset();
		}
		
		//Signal service that we have finished measuring
	    Message msg = new Message();
	    msg.obj = SharedPreferences.ON_SENSOR_COMPLETION; 
	    completionHandler.dispatchMessage(msg);
	    
		//SharedData.getInstance().addDebugDataLog_t("SensorMain.stopSampling");
	}
	
	/**
	 * Builds a data string on the form:
	 * time;Calculated_std_deviation;
	 * data_x[0];data_y[0];data_z[0];<cr lf>
	 * data_x[1];data_y[1];data_z[1];<cr lf>
	 * ...
	 * @param calculatedResult
	 * @param data_x
	 * @param data_y
	 * @param data_z
	 */
	private void exportAccelerometerData(String time, String rating, List<Float> data_x, List<Float> data_y, List<Float> data_z){

		StringBuilder bldr = new StringBuilder();
		assert(data_x.size() == data_y.size() && data_x.size() == data_z.size());
		
		bldr.append(time);
		bldr.append(';');
		bldr.append(rating);
		bldr.append(';');
		//append a carriage return / linefeed
		bldr.append("\r\n");
		
		//iterate trough arrays and write to string builder
		for(int i=0 ; i<data_x.size(); i++){
			Float dataVal = round(data_x.get(i), 3, BigDecimal.ROUND_HALF_UP);
			bldr.append(dataVal);
			bldr.append(';');
			dataVal = round(data_y.get(i), 3, BigDecimal.ROUND_HALF_UP);
			bldr.append(dataVal);
			bldr.append(';');
			dataVal = round(data_z.get(i), 3, BigDecimal.ROUND_HALF_UP);
			bldr.append(dataVal);
			bldr.append(';');
			//append a carriage return / linefeed
			bldr.append("\r\n");
		}
		//append a carriage return / linefeed
		bldr.append("\r\n");
		
	
		String dataStr = bldr.toString();
		//replace all '.' with ',' as this is the danish way
		String dataStr_locale = dataStr.replace('.', ',');
		
		
		Utility.writeFile(baseFolder, SharedPreferences.RAW_DATA_FILENAME, dataStr_locale, true /*Allways append*/);
		//Log.w("TIME","exportAccelerometerData ends. Time:" + ((Long)(SystemClock.uptimeMillis()-t)).toString() );
	}

	/**
	 * Initialise the raw data file to an empty file
	 */
	private void initializeAccelerometerDataFile(){
		String str = new String();
		str = "Time_of_measurement;calculated_std_deviation;\r\naccelerometer_x[0];accelerometer_y[0];accelerometer_z[0];\r\n";
		Utility.writeFile(baseFolder, SharedPreferences.RAW_DATA_FILENAME, str, false /*clear file content*/);
	}	
	
	/**
	 * Helper function that rounds floats to a number of decimals
	 * @param unrounded
	 * @param precision
	 * @param roundingMode
	 * @return
	 */
	private float round(float unrounded, int precision, int roundingMode)
	{
	    BigDecimal bd = new BigDecimal(unrounded);
	    BigDecimal rounded = bd.setScale(precision, roundingMode);
	    return rounded.floatValue();
	}
	/**
	 * Helper function that rounds floats to a number of decimals
	 * @param unrounded
	 * @param precision
	 * @param roundingMode
	 * @return
	 */
	private double round(double unrounded, int precision, int roundingMode)
	{
	    BigDecimal bd = new BigDecimal(unrounded);
	    BigDecimal rounded = bd.setScale(precision, roundingMode);
	    return rounded.doubleValue();
	}

	/**
	 * Returns true if the dataset is large enough 
	 * We define large enough 
	 * @return
	 */
	private boolean isDatasetLargeEnough(List<Float> data) {
		final long SAMPLE_COUNT = DEVIATION_SAMPLE_INTERVAL / SharedPreferences.ACCELEROMETER_SAMPLE_PERIOD; 
		long maxSamples = SharedPreferences.SAMPLE_PERIOD_ACCELEROMETER / SharedPreferences.ACCELEROMETER_SAMPLE_PERIOD;
		//we require more samples than the samples in one DEVIATION_SAMPLE_INTERVAL
		if(data.size() < SAMPLE_COUNT)
			return false;
		//we require 90% of the samples in a complete sampling set
		if(data.size() < Math.round((float)maxSamples * 0.9))
			return false;
		return true;
	}

	
	
	static final int SAMPLE_INTERVAL 			= (int)SharedPreferences.ACCELEROMETER_SAMPLE_PERIOD;
	static final int STD_DEV_WINDOW_INTERVAL 	= 2000;
	static final int DEVIATION_SAMPLE_INTERVAL 	= STD_DEV_WINDOW_INTERVAL/2;//makes us overlap 2 windows
	static final int WINDOW_SAMPLES 			= STD_DEV_WINDOW_INTERVAL/SAMPLE_INTERVAL;
	static final int DEVIATION_SAMPLES 			= DEVIATION_SAMPLE_INTERVAL/SAMPLE_INTERVAL;
	//static final float MOTION_LIMIT_STD_DEVIATION	= 0.2F;
	static final float MIN_MAX_LIMIT	= 5.0F;
	

	public enum enumMovementRating {
		LOW, MEDIUM, HIGH
	}
	/**
	 * Weighs the result and 
	 * @param x
	 * @param y
	 * @param z
	 * @return
	 */
	private enumMovementRating weighData(List<Float> x, List<Float> y, List<Float> z){
		List<Float> results = interpretAccelerometerData(x, y, z);
		final float resultsSize = (float)results.size();  
		assert(results.size() != 0);
		final float maxLow = 1;
		final float maxMedium = 5;
		//LOW => 5 periods can be above the 'maxLow' limit (16.6% (17.5%) of the time)
		//  however max 1 periods can be above the 'maxMedium' limit (3.33% (3.5%) of the time)
		//MEDIUM => 4 periods can be above the 'maxMedium' limit (13.3% (13.8%) of the time)
		//HIGH => everything else
		// numbers in paranthesis is if we only have 29 periods
		float countOverLow = 0;
		float countOverMed = 0;
		for(Float val : results){
			if(val > maxLow)
				countOverLow+=1;
			if(val > maxMedium)
				countOverMed+=1;
		}

		//calculate rating based on %
		if(100.0*countOverMed/resultsSize > 13.8)
			return enumMovementRating.HIGH;
		
		if(100.0*countOverLow/resultsSize < 17.5 && 100.0*countOverMed/resultsSize < 3.5)
			return enumMovementRating.LOW;
		return enumMovementRating.MEDIUM;
	}
	
	private List<Float> interpretAccelerometerData(List<Float> x, List<Float> y, List<Float> z) {
		assert(x.size() == y.size() && x.size() == z.size());
		List<Float> results = new ArrayList<Float>();
		

//		//testing
//		x.clear();y.clear();z.clear();
//		for(int i=0 ; i<x_data.length ; i++){
//			x.add(x_data[i]);
//			y.add(y_data[i]);
//			z.add(z_data[i]);
//		}
		
		
		//run trough all complete sets (SAMPLE_COUNT) and calculate std. deviation for each set
		for(int i=0 ; i+WINDOW_SAMPLES <= x.size() ; i+=WINDOW_SAMPLES){
			Float global_max_value = Float.NEGATIVE_INFINITY;
			
			List<Float> x_windows_data = x.subList(i, i+WINDOW_SAMPLES);
			List<Float> y_windows_data = y.subList(i, i+WINDOW_SAMPLES);
			List<Float> z_windows_data = z.subList(i, i+WINDOW_SAMPLES);
			
			float min_x=Float.POSITIVE_INFINITY, min_y=Float.POSITIVE_INFINITY, min_z=Float.POSITIVE_INFINITY;
			float max_x=Float.NEGATIVE_INFINITY, max_y=Float.NEGATIVE_INFINITY, max_z=Float.NEGATIVE_INFINITY;
			for(int j=0 ; j<WINDOW_SAMPLES ; j++){
				if(x_windows_data.get(j) > max_x)
					max_x = x_windows_data.get(j); 
				if(x_windows_data.get(j) < min_x)
					min_x = x_windows_data.get(j); 
				
				if(y_windows_data.get(j) > max_y)
					max_y = y_windows_data.get(j); 
				if(y_windows_data.get(j) < min_y)
					min_y = y_windows_data.get(j); 
				
				if(z_windows_data.get(j) > max_z)
					max_z = z_windows_data.get(j); 
				if(z_windows_data.get(j) < min_z)
					min_z = z_windows_data.get(j); 
			}
			if((max_x - min_x) > global_max_value)
				global_max_value = max_x - min_x;
			if((max_y - min_y) > global_max_value)
				global_max_value = max_y - min_y;
			if((max_z - min_z) > global_max_value)
				global_max_value = max_z - min_z;
			results.add(global_max_value);
		}
		return results;
	}
}
